/*
 * Copyright (C) 2004 Johan Maasing johan at zoom.nu Licensed under the Apache
 * License, Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
 * or agreed to in writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package nu.zoom.swing.field;

import java.text.*;
import java.awt.Toolkit;
import javax.swing.*;
import javax.swing.text.*;

/**
 * This class implements a swing JTextField which only accepts float numbers. It
 * does so by checking anything entered in the field and at all times it must be
 * parsable as a float value. This means you can't enter plain text or other
 * symbols in the field. It has some utility methods to put and retrieve the
 * float value directly. <BR>
 * Default values for the field are approx. 11 columns width. The formatter used
 * is initialized to display max 6 integer numbers, max 6 fraction numbers and
 * min 1 fraction number. This will be noticeable by e.g. using <CODE>
 * setValue(0.123456789f) ;</CODE> where the field will truncate the value to
 * 0.123456. To change these default values you can retrieve the formatter by
 * the get/setFormatter methods. <BR>
 * In the spirit of most of Swing this class is not thread safe.
 * 
 * @author $Author: johan $
 * @version $Revision: 1.2 $
 */
@SuppressWarnings("serial")
public class FloatField extends JTextField {
	protected DecimalFormat formatter;

	/**
	 * Constructs a FloatField with the default values and initialized to 0.0.
	 */
	public FloatField() {
		super(11);
		setDocument(new TexCoordDocument());

		formatter = new DecimalFormat();
		formatter.setMaximumFractionDigits(6);
		formatter.setMinimumFractionDigits(1);
		formatter.setMaximumIntegerDigits(6);
		formatter.setDecimalSeparatorAlwaysShown(true);
		formatter.setGroupingUsed(false);

		setValue(0.0f);
	}

	/**
	 * Constructs a FloatField with the default values and initialized with a
	 * value.
	 * 
	 * @param value
	 *            The intitial float value the field will display.
	 */
	public FloatField(float value) {
		super(11);
		setDocument(new TexCoordDocument());

		formatter = new DecimalFormat();
		formatter.setMaximumFractionDigits(6);
		formatter.setMinimumFractionDigits(1);
		formatter.setMaximumIntegerDigits(6);
		formatter.setDecimalSeparatorAlwaysShown(true);
		formatter.setGroupingUsed(false);

		setValue(value);
	}

	/**
	 * Constructs a FloatField with the default values and initialized with a
	 * value.
	 * 
	 * @param value
	 *            The intitial float value the field will display.
	 * @param enabled
	 *            Indicates if the field should initially be enabled for
	 *            editing.
	 */
	public FloatField(float value, boolean enabled) {
		super(11);
		setDocument(new TexCoordDocument());

		formatter = new DecimalFormat();
		formatter.setMaximumFractionDigits(6);
		formatter.setMinimumFractionDigits(1);
		formatter.setMaximumIntegerDigits(6);
		formatter.setDecimalSeparatorAlwaysShown(true);
		formatter.setGroupingUsed(false);

		setValue(value);
		setEnabled(enabled);
	}

	/**
	 * Retrieves the float value displayed in the field. There is one case where
	 * the field could actually be considered legal but still not contain a
	 * float value, that is if the field have been emptied in which case an
	 * exception will be thrown.
	 * 
	 * @return The value currently in the field.
	 * @throws ParseException
	 *             If the field does not contain a legal float value.
	 */
	public float getValue() throws ParseException {
		Number ret = formatter.parse(getText());
		return ret.floatValue();
	}

	/**
	 * Set the value the field displays.
	 * 
	 * @param value
	 *            The new value the field should display.
	 */
	public void setValue(float value) {
		String t = formatter.format(value);
		// This is the verbatim of the setText in JTextComponent
		try {
			Document doc = getDocument();
			doc.remove(0, doc.getLength());
			doc.insertString(0, t, null);
		} catch (BadLocationException e) {
			getToolkit().beep();
		}
	}

	/**
	 * Get a reference to the formatter the field uses to parse and format to
	 * displayed number.
	 * 
	 * @return The DecimalFormat object this field uses to format the output and
	 *         validate the input.
	 */
	public DecimalFormat getFormat() {
		return formatter;
	}

	/**
	 * Set the formatter the field uses to parse and format numbers.
	 * 
	 * @param format
	 *            The DecimalFormat the field should use in parsing and
	 *            formatting the value.
	 */
	public void setFormat(DecimalFormat format) {
		formatter = format;
	}

	/**
	 * Overriden document to handle the validation of the value.
	 */
	class TexCoordDocument extends PlainDocument {
		ParsePosition ppos = new ParsePosition(0);

		TexCoordDocument() {
			super();
		}

		public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
			// Check special cases
			if ((str == null) || (str == "")) {
				// Empty string or null is special case.
				// They don't parse to a legal number but should be
				// allowed anyway to clear the text field
				int len = getLength();
				try {
					remove(0, len);
				} catch (Exception exc) {
					System.err.println(exc.toString());
				}
				return;
			}

			// Ordinary case, construct the proposed resulting string
			// and see if it parses to a number.
			// If the proposed result is not a number, beep the speaker
			// and ignore the change.
			String currentText = getText(0, getLength());
			String beforeOffset = currentText.substring(0, offs);
			String afterOffset = currentText.substring(offs, currentText.length());
			String proposedResult = beforeOffset + str + afterOffset;

			ppos.setIndex(0);
			formatter.parseObject(proposedResult, ppos);
			// Now check if the entire string could be parsed
			if (ppos.getIndex() == proposedResult.length()) {
				// Parsed OK
				super.insertString(offs, str, a);
			} else {
				// Parse failed
				Toolkit.getDefaultToolkit().beep();
			}
		}

		public void remove(int offs, int len) throws BadLocationException {
			String currentText = getText(0, getLength());
			String beforeOffset = currentText.substring(0, offs);
			String afterOffset = currentText.substring(len + offs, currentText.length());
			String proposedResult = beforeOffset + afterOffset;

			if (proposedResult.length() == 0) {
				// Just empty the document
				super.remove(offs, len);
			} else {
				ppos.setIndex(0);
				formatter.parseObject(proposedResult, ppos);
				// Now check if the entire string could be parsed
				if (ppos.getIndex() == proposedResult.length()) {
					// Parsed OK
					super.remove(offs, len);
				} else {
					// Parse failed
					Toolkit.getDefaultToolkit().beep();
				}
			}
		}
	}
}